<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace mustache;

\switcher\utils\HttpHelper::appendResponseHeader('X-Powered-By', 'Mustache');

/**
 * This class is the entry point towards the Mustache language. It parses templates
 * and renders them provided a context and a partial loader. The context can be of
 * any type, but must be consistent with the template's expectations.
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @since 1.0.0
 * @see PartialLoader
 * @see ParseException
 */
final class Parser
{
	/**
	 * Used to read from input and write to output.
	 * @var Stream
	 */
	private $stream;
	
	/**
	 * The section stack keeps track of the section nesting.
	 * @var SectionStack
	 */
	private $sectionStack;
	
	/**
	 * Used to load partials if needed.
	 * @var PartialLoader
	 */
	private $partialLoader;
	
	/**
	 * The output where the template is rendered.
	 * @var string
	 */
	private $output = '';
	
	/**
	 * Indicates if rendering has started.
	 * @var bool
	 */
	private $rendering = false;
	
	/**
	 * Associates a Mustache symbol to a renderer method.
	 * @var array
	 */
	private static $tagRenderers = array(
		''  => 'renderVariable',
		'&' => 'renderUnescapedVariable',
		'#' => 'openSection',
		'^' => 'openInvertedSection',
		'/' => 'closeSection',
		'>' => 'renderPartial',
		'=' => 'changeDelimiter'
	);
	
	public $charset = null;
	
	/**
	 * Constructor.
	 * 
	 * @param string $input The mustache template
	 * @param mixed $context The template context
	 * @param PartialLoader $partialLoader The partial loader
	 * @throws InvalidArgumentException If the $input is not a string
	 */
	public function __construct($input, $context, PartialLoader $partialLoader)
	{
		$this->stream = new Stream($input);
		$this->sectionStack = new SectionStack($context);
		$this->partialLoader = $partialLoader;
	}
	
	private function checkRendering()
	{
		if ($this->rendering)
		{
			throw new \LogicException('The parser has already been used.');
		}
		
		$this->rendering = true;
	}
	
	/**
	 * Render the template with the given context.
	 * 
	 * @return string The template rendered accordingly to the context
	 * @throws ParseException If the template or context aren't both valid
	 */
	public function render()
	{
		$this->checkRendering();
		
		while ( $this->stream->hasNext() )
		{
			$this->renderTag();
		}
		
		if ($this->sectionStack->size() > 1)
		{
			$message = 'Section not closed : ' . $this->sectionStack->section()->name;
			throw $this->stream->newParseException($message);
		}
		
		return $this->output;
	}
	
	private function appendString($string)
	{
		if ($this->sectionStack->section()->rendered)
		{
			$this->output .= $string;
		}
	}
	
	private function renderTag()
	{
		$tag = $this->stream->getNextTag();
		
		$this->appendString( $this->stream->readToTag($tag) );
		
		if ($tag !== false && $tag->type !== '!')
		{
			$renderer = self::$tagRenderers[$tag->type];
			$this->$renderer($tag);
		}
	}
	
	private function extractVariableName($tag)
	{
		if (preg_match('~^\s*(\.|([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*))\s*$~i', $tag->content, $matches) == 0)
		{
			throw $this->stream->newTagParseException('Illegal variable name : ' . $tag->content, $tag);
		}
		
		$tag->content = $matches[1];
	}
	
	private function renderVariable($tag)
	{
		$this->extractVariableName($tag);
		$this->appendString( htmlentities($this->sectionStack->getVariable($tag), ENT_COMPAT, $this->charset) );
	}
	
	private function renderUnescapedVariable($tag)
	{
		$this->extractVariableName($tag);
		$this->appendString($this->sectionStack->getVariable($tag));
	}
	
	private function openSection($tag)
	{
		$this->extractVariableName($tag);
		$this->sectionStack->openSection($tag);
	}
	
	private function openInvertedSection($tag)
	{
		$this->extractVariableName($tag);
		$this->sectionStack->openInvertedSection($tag);
	}
	
	private function closeSection($tag)
	{
		$this->extractVariableName($tag);
		
		if ($tag->content != $this->sectionStack->section()->name)
		{
			throw $this->stream->newTagParseException('Unexpexted section close : ' . $tag->content, $tag);
		}
		
		$position = $this->sectionStack->closeSection();
		$this->stream->rewind( $position );
	}
	
	private function renderPartial($tag)
	{
		if ($this->sectionStack->section()->rendered)
		{
			$partial = $this->loadPartial($tag);
			
			$mustache = new Parser($partial, $this->sectionStack->section()->context(), $this->partialLoader);
			$mustache->charset = $this->charset;
			$this->appendString($mustache->render());
		}
	}
	
	private function loadPartial($tag)
	{
		$indentation = $this->getPartialIndentation($tag);
		$partial = $this->partialLoader->loadPartial( trim($tag->content) );
		
		return \switcher\utils\FileHelper::indentString($partial, $indentation);
	}
	
	private function getPartialIndentation($tag)
	{
		preg_match('~^[\\t ]*~', $tag->tag, $matches);
		array_push($matches, '');
		
		return $matches[0];
	}
	
	private function changeDelimiter($tag)
	{
		if ($this->sectionStack->section()->name != null)
		{
			$message = 'Unexpected delimiter change within a section : ' . $this->sectionStack->section()->name;
			throw $this->stream->newTagParseException($message, $tag);
		}
		
		$this->stream->setDelimiter($tag);
	}
}

?>